Dybdegående kig på JavaScript Async Iterator-ydelse. Lær, hvordan du profilerer, optimerer og accelererer behandling af datastrømme for forbedret applikationsydelse.
Ydelsesprofilering af JavaScript Async Iterator: Hastighed for Behandling af Datastrømme
JavaScripts asynkrone kapabiliteter har revolutioneret webudvikling og muliggjort meget responsive og effektive applikationer. Blandt disse fremskridt er Async Iterators dukket op som et stærkt værktøj til håndtering af datastrømme, der tilbyder en fleksibel og performant tilgang til databehandling. Dette blogindlæg dykker ned i nuancerne af Async Iterator-ydelse og giver en omfattende guide til profilering, optimering og maksimering af hastigheden for behandling af datastrømme. Vi vil udforske forskellige teknikker, benchmark-metodologier og eksempler fra den virkelige verden for at give udviklere den viden og de værktøjer, der er nødvendige for at bygge højtydende, skalerbare applikationer.
Forståelse af Async Iterators
Før vi dykker ned i ydelsesprofilering, er det afgørende at forstå, hvad Async Iterators er, og hvordan de fungerer. En Async Iterator er et objekt, der giver en asynkron grænseflade til at forbruge en sekvens af værdier. Dette er især nyttigt, når man arbejder med potentielt uendelige eller store datasæt, der ikke kan indlæses i hukommelsen på én gang. Async Iterators er fundamentale for designet af flere JavaScript-funktioner, herunder Web Streams API.
I sin kerne implementerer en Async Iterator Iterator-protokollen med en async next()-metode. Denne metode returnerer et Promise, der resolver til et objekt med to egenskaber: value (det næste element i sekvensen) og done (en boolean, der angiver, om sekvensen er fuldført). Denne asynkrone natur tillader ikke-blokerende operationer, hvilket forhindrer brugergrænsefladen i at fryse, mens man venter på data.
Overvej et simpelt eksempel på en Async Iterator, der genererer tal:
class NumberGenerator {
constructor(limit) {
this.limit = limit;
this.current = 0;
}
async *[Symbol.asyncIterator]() {
while (this.current < this.limit) {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate asynchronous operation
yield this.current++;
}
}
}
async function consumeGenerator() {
const generator = new NumberGenerator(5);
for await (const number of generator) {
console.log(number);
}
}
consumeGenerator();
I dette eksempel bruger NumberGenerator-klassen en generatorfunktion (angivet med *), der yielder tal asynkront. for await...of-løkken itererer gennem generatoren og forbruger hvert tal, efterhånden som det bliver tilgængeligt. setTimeout-funktionen simulerer en asynkron operation, såsom at hente data fra en server eller behandle en stor fil. Dette demonstrerer det grundlæggende princip: hver iteration venter på, at en asynkron opgave afsluttes, før den behandler den næste værdi.
Hvorfor ydelsesprofilering er vigtigt for Async Iterators
Selvom Async Iterators tilbyder betydelige fordele inden for asynkron programmering, kan ineffektive implementeringer føre til ydelsesmæssige flaskehalse, især ved håndtering af store datasæt eller komplekse behandlingspipelines. Ydelsesprofilering hjælper med at identificere disse flaskehalse, hvilket giver udviklere mulighed for at optimere deres kode for hastighed og effektivitet.
Fordelene ved ydelsesprofilering inkluderer:
- Identificering af langsomme operationer: At finde præcis de dele af koden, der bruger mest tid og flest ressourcer.
- Optimering af ressourceforbrug: At forstå, hvordan hukommelse og CPU udnyttes under behandling af datastrømme og optimere for effektiv ressourcetildeling.
- Forbedring af skalerbarhed: At sikre, at applikationer kan håndtere stigende datamængder og brugerbelastninger uden forringelse af ydeevnen.
- Forøgelse af responsivitet: At garantere en glat brugeroplevelse ved at minimere latenstid og forhindre, at brugergrænsefladen fryser.
Værktøjer og teknikker til profilering af Async Iterators
Der findes flere værktøjer og teknikker til at profilere ydelsen af Async Iterators. Disse værktøjer giver værdifuld indsigt i udførelsen af din kode og hjælper dig med at finde forbedringsområder.
1. Browserens udviklingsværktøjer
Moderne webbrowsere, såsom Chrome, Firefox og Edge, er udstyret med indbyggede udviklingsværktøjer, der inkluderer kraftfulde profileringsmuligheder. Disse værktøjer giver dig mulighed for at optage og analysere ydeevnen af JavaScript-kode, herunder Async Iterators. Sådan bruger du dem effektivt:
- Performance-fanen: Brug 'Performance'-fanen til at optage en tidslinje for din applikations eksekvering. Start optagelsen, før koden, der bruger Async Iterator, køres, og stop den bagefter. Tidslinjen vil visualisere CPU-brug, hukommelsestildeling og hændelsestiminger.
- Flame Charts: Analysér flame chartet for at identificere tidskrævende funktioner. Jo bredere søjlen er, jo længere tid tog funktionen at eksekvere.
- Funktionsprofilering: Dyk ned i specifikke funktionskald for at forstå deres eksekveringstid og ressourceforbrug.
- Hukommelsesprofilering: Overvåg hukommelsesforbruget for at identificere potentielle hukommelseslækager eller ineffektive mønstre for hukommelsestildeling.
Eksempel: Profilering i Chrome Developer Tools
- Åbn Chrome Developer Tools (højreklik på siden og vælg 'Inspicer' eller tryk på F12).
- Naviger til 'Performance'-fanen.
- Klik på 'Record'-knappen (cirklen).
- Udløs koden, der bruger din Async Iterator.
- Klik på 'Stop'-knappen (firkanten).
- Analysér flame chartet, funktionstiminger og hukommelsesforbrug for at identificere ydelsesmæssige flaskehalse.
2. Profilering i Node.js med `perf_hooks` og `v8-profiler-node`
For server-side applikationer, der bruger Node.js, kan du bruge `perf_hooks`-modulet, som er en del af Node.js-kernen, og/eller `v8-profiler-node`-pakken, som giver mere avancerede profileringsmuligheder. Dette giver dybere indsigt i V8-motorens eksekvering.
Brug af `perf_hooks`
`perf_hooks`-modulet tilbyder et Performance API, der giver dig mulighed for at måle ydeevnen af forskellige operationer, herunder dem, der involverer Async Iterators. Du kan bruge `performance.now()` til at måle den forløbne tid mellem specifikke punkter i din kode.
const { performance } = require('perf_hooks');
async function processData() {
const startTime = performance.now();
// Your Async Iterator code here
const endTime = performance.now();
console.log(`Processing time: ${endTime - startTime}ms`);
}
Brug af `v8-profiler-node`
Installer pakken med npm: `npm install v8-profiler-node`
const v8Profiler = require('v8-profiler-node');
const fs = require('fs');
async function processData() {
v8Profiler.setSamplingInterval(1000); // Set the sampling interval in microseconds
v8Profiler.startProfiling('AsyncIteratorProfile');
// Your Async Iterator code here
const profile = v8Profiler.stopProfiling('AsyncIteratorProfile');
profile
.export()
.then((result) => {
fs.writeFileSync('async_iterator_profile.cpuprofile', result);
profile.delete();
console.log('CPU profile saved to async_iterator_profile.cpuprofile');
});
}
Denne kode starter en CPU-profileringssession, kører din Async Iterator-kode og stopper derefter profileringen, hvilket genererer en CPU-profilfil (i .cpuprofile-format). Du kan derefter bruge Chrome DevTools (eller et lignende værktøj) til at åbne CPU-profilen og analysere ydelsesdataene, herunder flame charts og funktionstiminger.
3. Benchmarking-biblioteker
Benchmarking-biblioteker, såsom `benchmark.js`, giver en struktureret måde at måle ydeevnen af forskellige kodestykker og sammenligne deres eksekveringstider. Dette er især værdifuldt for at sammenligne forskellige implementeringer af Async Iterators eller identificere effekten af specifikke optimeringer.
Eksempel med `benchmark.js`
const Benchmark = require('benchmark');
// Sample Async Iterator implementation
async function* asyncGenerator(count) {
for (let i = 0; i < count; i++) {
await new Promise(resolve => setTimeout(resolve, 1));
yield i;
}
}
const suite = new Benchmark.Suite();
suite
.add('AsyncIterator', {
defer: true,
fn: async (deferred) => {
for await (const item of asyncGenerator(100)) {
// Simulate processing
}
deferred.resolve();
}
})
.on('cycle', (event) => {
console.log(String(event.target));
})
.on('complete', () => {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ async: true });
Dette eksempel opretter en benchmark-suite, der måler ydeevnen af en Async Iterator. `add`-metoden definerer koden, der skal benchmarkes, og `on('cycle')`- og `on('complete')`-hændelserne giver feedback om benchmarkets fremskridt og resultater.
Optimering af Async Iterator-ydelse
Når du har identificeret ydelsesmæssige flaskehalse, er det næste skridt at optimere din kode. Her er nogle nøgleområder at fokusere på:
1. Reducer asynkron overhead
Asynkrone operationer, såsom netværksanmodninger og fil-I/O, er i sagens natur langsommere end synkrone operationer. Minimer antallet af asynkrone kald inden i din Async Iterator for at reducere overhead. Overvej teknikker som batching og parallel behandling.
- Batching: I stedet for at behandle enkelte elementer ét ad gangen, kan du gruppere dem i batches og behandle disse batches asynkront. Dette reducerer antallet af asynkrone kald.
- Parallel behandling: Hvis det er muligt, kan du behandle elementer parallelt ved hjælp af teknikker som `Promise.all()` eller worker threads. Vær dog opmærksom på ressourcebegrænsninger og potentialet for øget hukommelsesforbrug.
2. Optimer logikken for databehandling
Behandlingslogikken inden i din Async Iterator kan have en betydelig indvirkning på ydeevnen. Sørg for, at din kode er effektiv og undgår unødvendige beregninger.
- Undgå unødvendige operationer: Gennemgå din kode for at identificere eventuelle unødvendige operationer eller beregninger.
- Brug effektive algoritmer: Vælg effektive algoritmer og datastrukturer til behandling af data. Overvej at bruge optimerede biblioteker, hvor det er muligt.
- Lazy Evaluation: Anvend lazy evaluation-teknikker for at undgå at behandle data, der ikke er nødvendige. Dette kan være særligt effektivt ved håndtering af store datasæt.
3. Effektiv hukommelseshåndtering
Hukommelseshåndtering er afgørende for ydeevnen, især ved håndtering af store datasæt. Ineffektiv hukommelsesbrug kan føre til forringelse af ydeevnen og potentielle hukommelseslækager.
- Undgå at holde store objekter i hukommelsen: Sørg for at frigive objekter fra hukommelsen, når du er færdig med dem. Hvis du for eksempel behandler store filer, skal du streame indholdet i stedet for at indlæse hele filen i hukommelsen på én gang.
- Brug Generators og Iterators: Generators og Iterators er hukommelseseffektive, især Async Iterators. De behandler data efter behov og undgår dermed at skulle indlæse hele datasættet i hukommelsen.
- Overvej datastrukturer: Brug passende datastrukturer til at gemme og manipulere data. For eksempel kan brug af et `Set` give hurtigere opslagstider sammenlignet med at iterere gennem et array.
4. Strømlining af Input/Output (I/O)-operationer
I/O-operationer, såsom læsning fra eller skrivning til filer, kan være betydelige flaskehalse. Optimer disse operationer for at forbedre den samlede ydeevne.
- Brug bufferet I/O: Bufferet I/O kan reducere antallet af individuelle læse/skrive-operationer, hvilket forbedrer effektiviteten.
- Minimer diskadgang: Undgå unødvendig diskadgang, hvis det er muligt. Overvej at cache data eller bruge in-memory-lagring til ofte tilgåede data.
- Optimer netværksanmodninger: For netværksbaserede Async Iterators skal du optimere netværksanmodninger ved hjælp af teknikker som connection pooling, request batching og effektiv dataserielisering.
Praktiske eksempler og optimeringer
Lad os se på nogle praktiske eksempler for at illustrere, hvordan man anvender de optimeringsteknikker, der er diskuteret ovenfor.
Eksempel 1: Behandling af store JSON-filer
Antag, at du har en stor JSON-fil, du skal behandle. At indlæse hele filen i hukommelsen er ineffektivt. Ved hjælp af Async Iterators kan vi behandle filen i bidder.
const fs = require('fs');
const readline = require('readline');
async function* readJsonLines(filePath) {
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity // To recognize all instances of CR LF ('\r\n') as a single line break
});
for await (const line of rl) {
try {
const jsonObject = JSON.parse(line);
yield jsonObject;
} catch (error) {
console.error('Error parsing JSON:', error);
// Handle the error (e.g., skip the line, log the error)
}
}
}
async function processJsonData(filePath) {
for await (const data of readJsonLines(filePath)) {
// Process each JSON object here
console.log(data.someProperty);
}
}
// Example Usage
processJsonData('large_data.json');
Optimering:
- Dette eksempel bruger `readline` til at læse filen linje for linje, hvilket undgår behovet for at indlæse hele filen i hukommelsen.
- `JSON.parse()`-operationen udføres for hver linje, hvilket holder hukommelsesforbruget håndterbart.
Eksempel 2: Streaming af data fra web-API
Forestil dig et scenarie, hvor du henter data fra et web-API, der returnerer data i bidder eller paginerede svar. Async Iterators kan håndtere dette elegant.
async function* fetchPaginatedData(apiUrl) {
let nextPageUrl = apiUrl;
while (nextPageUrl) {
const response = await fetch(nextPageUrl);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
for (const item of data.results) { // Assuming data.results contains the actual data items
yield item;
}
nextPageUrl = data.next; // Assuming the API provides a 'next' URL for pagination
}
}
async function consumeApiData(apiUrl) {
for await (const item of fetchPaginatedData(apiUrl)) {
// Process each data item here
console.log(item);
}
}
// Example usage:
consumeApiData('https://api.example.com/data'); // Replace with actual API URL
Optimering:
- Funktionen håndterer paginering elegant ved gentagne gange at hente den næste side med data, indtil der ikke er flere sider.
- Async Iterators giver applikationen mulighed for at begynde at behandle dataelementer, så snart de modtages, uden at vente på, at hele datasættet er downloadet.
Eksempel 3: Datatransformations-pipelines
Async Iterators er stærke til datatransformations-pipelines, hvor data flyder gennem en række asynkrone operationer. For eksempel kan du transformere data hentet fra et API, udføre filtrering og derefter gemme de behandlede data i en database.
// Mock Data Source (simulating API response)
async function* fetchData() {
yield { id: 1, value: 'abc' };
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate delay
yield { id: 2, value: 'def' };
await new Promise(resolve => setTimeout(resolve, 100));
yield { id: 3, value: 'ghi' };
}
// Transformation 1: Uppercase the value
async function* uppercaseTransform(source) {
for await (const item of source) {
yield { ...item, value: item.value.toUpperCase() };
}
}
// Transformation 2: Filter items with id greater than 1
async function* filterTransform(source) {
for await (const item of source) {
if (item.id > 1) {
yield item;
}
}
}
// Transformation 3: Simulate saving to a database
async function saveToDatabase(source) {
for await (const item of source) {
// Simulate database write with a delay
await new Promise(resolve => setTimeout(resolve, 50));
console.log('Saved to database:', item);
}
}
async function runPipeline() {
const data = fetchData();
const uppercasedData = uppercaseTransform(data);
const filteredData = filterTransform(uppercasedData);
await saveToDatabase(filteredData);
}
runPipeline();
Optimeringer:
- Modulært design: Hver transformation er en separat Async Iterator, hvilket fremmer genbrugelighed og vedligeholdelighed af koden.
- Lazy Evaluation: Data transformeres kun, når det forbruges af det næste trin i pipelinen. Dette undgår unødvendig behandling af data, der måske bliver filtreret fra senere.
- Asynkrone operationer inden for transformationer: Hver transformation, selv lagring i databasen, kan have asynkrone operationer som `setTimeout`, hvilket gør det muligt for pipelinen at køre uden at blokere andre opgaver.
Avancerede optimeringsteknikker
Ud over de grundlæggende optimeringer kan du overveje disse avancerede teknikker for yderligere at forbedre ydelsen af Async Iterators:
1. Brug af `ReadableStream` og `WritableStream` fra Web Streams API
Web Streams API'et giver kraftfulde primitiver til at arbejde med datastrømme, herunder `ReadableStream` og `WritableStream`. Disse kan bruges sammen med Async Iterators til højeffektiv behandling af datastrømme.
- `ReadableStream` repræsenterer en datastrøm, der kan læses fra. Du kan oprette en `ReadableStream` fra en Async Iterator eller bruge den som et mellemliggende trin i en pipeline.
- `WritableStream` repræsenterer en strøm, data kan skrives til. Denne kan bruges til at forbruge og fastholde outputtet fra en behandlingspipeline.
Eksempel: Integration med `ReadableStream`
async function* myAsyncGenerator() {
yield 'Data1';
yield 'Data2';
yield 'Data3';
}
async function runWithStreams() {
const asyncIterator = myAsyncGenerator();
const stream = new ReadableStream({
async pull(controller) {
const { value, done } = await asyncIterator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
}
});
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
console.log(value);
}
} finally {
reader.releaseLock();
}
}
runWithStreams();
Fordele: Streams API'et tilbyder optimerede mekanismer til håndtering af modtryk (backpressure), hvilket forhindrer en producent i at overvælde en forbruger, hvilket kan forbedre ydeevnen betydeligt og forhindre ressourceudmattelse.
2. Udnyttelse af Web Workers
Web Workers giver dig mulighed for at aflaste beregningstunge opgaver til separate tråde, hvilket forhindrer dem i at blokere hovedtråden og forbedrer din applikations responsivitet.
Sådan bruges Web Workers med Async Iterators:
- Aflast den tunge behandlingslogik fra Async Iteratoren til en Web Worker. Hovedtråden kan derefter kommunikere med workeren ved hjælp af beskeder.
- Workeren kan derefter modtage dataene, behandle dem og sende beskeder tilbage til hovedtråden med resultaterne. Hovedtråden vil derefter forbruge disse resultater.
Eksempel:
// Main thread (main.js)
const worker = new Worker('worker.js');
async function consumeData() {
worker.postMessage({ command: 'start', data: 'data_source' }); // Assuming data source is a file path or URL
worker.onmessage = (event) => {
if (event.data.type === 'data') {
console.log('Received from worker:', event.data.value);
} else if (event.data.type === 'done') {
console.log('Worker finished.');
}
};
}
// Worker thread (worker.js)
//Assume the asyncGenerator implementation is in worker.js as well, receiving commands
self.onmessage = async (event) => {
if (event.data.command === 'start') {
for await (const item of asyncGenerator(event.data.data)) {
self.postMessage({ type: 'data', value: item });
}
self.postMessage({ type: 'done' });
}
};
3. Caching og memoization
Hvis din Async Iterator gentagne gange behandler de samme data eller udfører beregningstunge operationer, kan du overveje at cache eller memoize resultaterne.
- Caching: Gem resultaterne af tidligere beregninger i en cache. Når det samme input stødes på igen, hentes resultatet fra cachen i stedet for at genberegne det.
- Memoization: Ligner caching, men bruges specifikt til rene funktioner. Memoize funktionen for at undgå at genberegne resultater for de samme inputs.
4. Omhyggelig fejlhåndtering
Robust fejlhåndtering er afgørende for Async Iterators, især i produktionsmiljøer.
- Implementer passende fejlhåndteringsstrategier. Indpak din Async Iterator-kode i `try...catch`-blokke for at fange fejl.
- Overvej konsekvenserne af fejl. Hvordan skal fejl håndteres? Skal processen stoppe helt, eller skal fejl logges, og behandlingen fortsætte?
- Log detaljerede fejlmeddelelser. Log fejlene, herunder relevant kontekstinformation, såsom inputværdier, stack traces og tidsstempler. Denne information er uvurderlig til debugging.
Benchmarking og test for ydeevne
Ydeevnetest er afgørende for at validere effektiviteten af dine optimeringer og sikre, at dine Async Iterators fungerer som forventet.
1. Etabler baseline-målinger
Før du anvender nogen optimeringer, skal du etablere en baseline-ydelsesmåling. Dette vil tjene som et referencepunkt for sammenligning af ydeevnen af din optimerede kode.
- Brug benchmarking-biblioteker. Mål eksekveringstiden for din kode ved hjælp af værktøjer som `benchmark.js` eller din browsers performance-fane.
- Mål forskellige scenarier. Test din kode med forskellige datasæt, datastørrelser og behandlingskompleksiteter for at få en omfattende forståelse af dens ydelseskarakteristika.
2. Iterativ optimering og test
Anvend optimeringer iterativt og re-benchmark din kode efter hver ændring. Denne iterative tilgang giver dig mulighed for at isolere effekterne af hver optimering og identificere de mest effektive teknikker.
- Optimer én ændring ad gangen. Undgå at lave flere ændringer samtidigt for at forenkle debugging og analyse.
- Re-benchmark efter hver optimering. Verificer, at ændringen forbedrede ydeevnen. Hvis ikke, skal du rulle ændringen tilbage og prøve en anden tilgang.
3. Kontinuerlig integration og ydelsesovervågning
Integrer ydelsestest i din kontinuerlige integrations (CI) pipeline. Dette sikrer, at ydeevnen overvåges kontinuerligt, og at ydelsesregressioner opdages tidligt i udviklingsprocessen.
- Integrer benchmarking i din CI-pipeline. Automatiser benchmarkingsprocessen.
- Overvåg ydelsesmålinger over tid. Spor nøgletal for ydeevne og identificer tendenser.
- Indstil ydelsestærskler. Indstil ydelsestærskler og bliv alarmeret, når de overskrides.
Virkelige anvendelser og eksempler
Async Iterators er utroligt alsidige og finder anvendelse i adskillige virkelige scenarier.
1. Behandling af store filer i e-handel
E-handelsplatforme håndterer ofte massive produktkataloger, lageropdateringer og ordrebehandling. Async Iterators muliggør effektiv behandling af store filer, der indeholder produktdata, prisinformation og kundeordrer, hvilket undgår hukommelsesudmattelse og forbedrer responsiviteten.
2. Realtids-datafeeds og streaming-applikationer
Applikationer, der kræver realtids-datafeeds, såsom finansielle handelsplatforme, sociale medieapplikationer og live dashboards, kan udnytte Async Iterators til at behandle streamingdata fra forskellige kilder, såsom API-endepunkter, meddelelseskøer og WebSocket-forbindelser. Dette giver brugeren øjeblikkelige dataopdateringer.
3. Dataekstraktion, -transformation og -indlæsning (ETL)-processer
Datapipelines involverer ofte at udtrække data fra flere kilder, transformere dem og indlæse dem i et data warehouse eller en database. Async Iterators giver en robust og skalerbar løsning til ETL-processer, hvilket giver udviklere mulighed for at behandle store datasæt effektivt.
4. Billed- og videobehandling
Async Iterators er nyttige til behandling af medieindhold. For eksempel kan Async Iterators i en videoredigeringsapplikation håndtere den kontinuerlige behandling af videoframes eller håndtere store billedbatches mere effektivt, hvilket sikrer en responsiv brugeroplevelse.
5. Chat-applikationer
I en chat-applikation er Async Iterators gode til at behandle meddelelser modtaget over en WebSocket-forbindelse. De giver dig mulighed for at behandle meddelelser, efterhånden som de ankommer, uden at blokere brugergrænsefladen og forbedrer responsiviteten.
Konklusion
Async Iterators er en fundamental del af moderne JavaScript-udvikling, der muliggør effektiv og responsiv behandling af datastrømme. Ved at forstå koncepterne bag Async Iterators, omfavne passende profileringsteknikker og anvende de optimeringsstrategier, der er beskrevet i dette blogindlæg, kan udviklere opnå betydelige ydelsesforbedringer og bygge applikationer, der er skalerbare og kan håndtere betydelige datamængder. Husk at benchmarke din kode, iterere på optimeringer og overvåge ydeevnen regelmæssigt. Den omhyggelige anvendelse af disse principper vil give udviklere mulighed for at skabe højtydende JavaScript-applikationer, hvilket fører til en mere behagelig brugeroplevelse over hele kloden. Fremtiden for webudvikling er i sagens natur asynkron, og at mestre Async Iterator-ydelse er en afgørende færdighed for enhver moderne udvikler.